refactor(attn): let the bucketer own the batched decode buckets - #810
refactor(attn): let the bucketer own the batched decode buckets#810sducouedic wants to merge 11 commits into
Conversation
|
👋 Hi! Thank you for contributing. We also recommend installing prek and configuring it to check your code before every local commit. |
SpyreAttentionMetadataBuilder built its own num_seqs and num_blocks ladders while SpyreAttnBucketer derived a num_blocks ladder of its own from the KV buckets. The two agree only on the default geometric KV buckets; under SPYRE_ATTN_KV_BUCKETS the builder dispatches onto low block counts that warmup never records, putting an Inductor compile in the serving path. Both ladders now come from the bucketer, so the set build() dispatches onto and the set warmup records are the same by construction. Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com> Co-authored-by: Jan van Lunteren <161835099+jvlunteren@users.noreply.github.com>
731f817 to
7e6e61a
Compare
tdoublep
left a comment
There was a problem hiding this comment.
Review by Claude Code, posted by me (@tdoublep).
Ran it locally: bucketer 53 passed, recorder 15 passed, and -k batched_decode 18 passed (that last one isn't in your test plan but it's the path being changed). Default buckets come out identical to before, so the default path is unaffected.
One correction for the description: warmup doesn't record any batched-decode variants at all. _record_one only traces _attn_fn, and SpyreAttnBucket has no num_seqs field, so variants() can't describe a decode variant. Those kernels still compile on first use in the serving path, before and after this change. The real win is fewer of those compiles plus a single source of truth — worth saying that instead, otherwise the next reader will assume decode is covered by warmup.
Rest is inline.
|
I also ran it through Claude an it additionally found those points:
mml=2048 [1, 2, 4, 8, 11] -> [2, 3, 6, 11]
|
Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com>
Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com>
Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com>
…eqs-ladder Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com>
The ladder starts at _MIN_BATCHED_SEQS since smaller batches take the per-seq loop. Folds the two near-duplicate ladder tests into one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com>
| # The attention KV buckets are powers of two starting at block_size; a | ||
| # non-power-of-two block size makes that ladder start off-grid, so block | ||
| # counts stop being a clean doubling sequence. | ||
| if aligned & (aligned - 1): | ||
| raise ValueError( | ||
| f"Block size must be a power of two for the Spyre paged attention " | ||
| f"backend, got {aligned}." | ||
| ) |
There was a problem hiding this comment.
this addresses comment from @bohnstingl:
Defaults do change for non-power-of-two block_size (highest priority)
The review confirms defaults are unchanged; that holds only for power-of-two block_size. At block_size=192 — supported, warned about in spyre_attn_bucketer.py:131-140, and tested in tests/runtime/test_spyre_attn_bucketer.py:77-82 — the ladder moves with no env override set:
| # Comma-separated kv_len buckets to record, unset uses the default buckets of | ||
| # powers of two from block_size up to max_model_len. | ||
| "SPYRE_ATTN_KV_BUCKETS": lambda: os.getenv("SPYRE_ATTN_KV_BUCKETS"), | ||
| # Comma-separated query_len buckets to record, unset uses the default buckets | ||
| # [1] + multiples of min(512, max_num_batched_tokens) up to max_num_batched_tokens. | ||
| "SPYRE_ATTN_QUERY_BUCKETS": lambda: os.getenv("SPYRE_ATTN_QUERY_BUCKETS"), | ||
| # Comma-separated num_seqs buckets for the batched decode kernel, unset uses the | ||
| # default buckets of powers of two from 4 up to max_num_seqs. | ||
| "SPYRE_ATTN_NUM_SEQS_BUCKETS": lambda: os.getenv("SPYRE_ATTN_NUM_SEQS_BUCKETS"), |
There was a problem hiding this comment.
I'm not sure anymore it is worth having env var to override the buckets for each axis, can be useful to speedup the warmup though
There was a problem hiding this comment.
I think it's useful to be able to override each axis if neded.
| decode_blocks = blocks_per_seq[:num_decode_seqs] | ||
| b_seqs = _find_bucket(num_decode_seqs, self._num_seqs_buckets) | ||
| b_blocks = _find_bucket(max(decode_blocks), self._num_blocks_buckets) | ||
| b_seqs = SpyreAttnBucketer._round_up( |
There was a problem hiding this comment.
Review by Claude Code, posted by me (@tdoublep).
Follow-up on the earlier thread about the two tuple copies: the drift is genuinely closed now, but the cleanup left three loose ends that all meet at these two lines.
_find_bucketat line 96 no longer has any callers — it can go.find_sequence_bucket(spyre_attn_bucketer.py:224), added in the same commit, is never called. These lines are exactly its use site.- As written this reaches into the bucketer's private
_num_seqs_buckets/_num_blocks_bucketsand calls_round_upas a static method on another object's state.self._attn_bucketer.find_sequence_bucket(num_seqs)plus a matchingfind_blocks_bucketwould keep the lookup behind the bucketer's API and make the new method live. Line 675 has the same static-call shape (via the public property) if you want them consistent.
tdoublep
left a comment
There was a problem hiding this comment.
left a few nits but otherwise good to go
joerunde
left a comment
There was a problem hiding this comment.
looks like a reasonable refactor to me too! 👍
Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com>
|
⚙️ Auto-merge enabled — dispatched Automated: enabling auto-merge requests the gate, so the PR is not left waiting on a check nobody asked for. Re-enabling auto-merge on this same commit will not dispatch again. |
🔄 spyre-test: runningTriggered by: /spyre-test comment Plan (build waves + dependencies, per arch)amd64 flowchart LR
subgraph Lamd64_0["amd64 L0 · 1 parallel"]
n_amd64_flex["flex ↺<br/>rpm · 21f9dbf1c01c"]
end
subgraph Lamd64_1["amd64 L1 · 3 parallel"]
n_amd64_aiu_toolbox_ibm_aiu_toolbox_e2e["aiu-toolbox/ibm-aiu-toolbox-e2e<br/>rpm · 6068750b7f59"]
n_amd64_libaiupti["libaiupti<br/>rpm · 47e615e55dc3"]
n_amd64_spyre_comms["spyre-comms<br/>rpm · cd26cd09b611"]
end
subgraph Lamd64_2["amd64 L2 · 1 parallel"]
n_amd64_spyre_backend_spyre_backend_dev["spyre-backend/spyre-backend-dev<br/>image · 00cdede56c88"]
end
subgraph Lamd64_3["amd64 L3 · 1 parallel"]
n_amd64_torch_spyre_torch_spyre_dev["torch-spyre/torch-spyre-dev<br/>image · dd7d61b76437"]
end
subgraph Lamd64_4["amd64 L4 · 1 parallel"]
n_amd64_hf_adapters_hf_adapters_dev["hf-adapters/hf-adapters-dev<br/>image · fa2a54db6cab"]
end
subgraph Lamd64_5["amd64 L5 · 1 parallel"]
n_amd64_spyre_inference_spyre_inference_dev["spyre-inference/spyre-inference-dev<br/>image · d98eba460635"]
end
n_amd64_flex --> n_amd64_spyre_comms
n_amd64_flex --> n_amd64_aiu_toolbox_ibm_aiu_toolbox_e2e
n_amd64_flex --> n_amd64_libaiupti
n_amd64_aiu_toolbox_ibm_aiu_toolbox_e2e --> n_amd64_spyre_backend_spyre_backend_dev
n_amd64_flex --> n_amd64_spyre_backend_spyre_backend_dev
n_amd64_libaiupti --> n_amd64_spyre_backend_spyre_backend_dev
n_amd64_spyre_comms --> n_amd64_spyre_backend_spyre_backend_dev
n_amd64_spyre_backend_spyre_backend_dev --> n_amd64_torch_spyre_torch_spyre_dev
n_amd64_torch_spyre_torch_spyre_dev --> n_amd64_hf_adapters_hf_adapters_dev
n_amd64_hf_adapters_hf_adapters_dev --> n_amd64_spyre_inference_spyre_inference_dev
classDef sPending fill:#eceff1,stroke:#90a4ae,color:#37474f
classDef sBuilding fill:#fff8e1,stroke:#f9a825,color:#5d4037,stroke-width:2px
classDef sOk fill:#e8f5e9,stroke:#43a047,color:#1b5e20
classDef sReused fill:#e3f2fd,stroke:#1e88e5,color:#0d47a1
classDef sFailed fill:#ffebee,stroke:#e53935,color:#b71c1c,stroke-width:2px
classDef sDropped fill:#f5f5f5,stroke:#bdbdbd,color:#9e9e9e
class n_amd64_flex sReused;
class n_amd64_aiu_toolbox_ibm_aiu_toolbox_e2e sBuilding;
class n_amd64_libaiupti sBuilding;
class n_amd64_spyre_comms sBuilding;
class n_amd64_spyre_backend_spyre_backend_dev sPending;
class n_amd64_torch_spyre_torch_spyre_dev sPending;
class n_amd64_hf_adapters_hf_adapters_dev sPending;
class n_amd64_spyre_inference_spyre_inference_dev sPending;
🔨 orch trigger-pr-validation · arches amd64 · fp amd64=e261d97c
|
Description
The batched decode path built its own
num_seqsandnum_blocksladders inSpyreAttentionMetadataBuilder, whileSpyreAttnBucketerderived anum_blocksladder of its own from the KV buckets. The two only agree on the default geometric KV buckets — underSPYRE_ATTN_KV_BUCKETSthe builder dispatches onto low block counts that warmup never records, so an Inductor compile lands in the serving path.Both ladders now come from the bucketer, making the set
build()dispatches onto and the set warmup records the same by construction.Related Issues
Test Plan
uv run pytest tests/runtime/test_spyre_attn_bucketer.py -m "not upstream"— 53 passed, 5 skippeduv run pytest tests/attention/test_spyre_attn_recorder.py -m "not upstream"— 15 passedbash format.sh— ruff check/format pass (tyis red onmaintoo, in this environment)Checklist
bash format.sh)Signed-off-by:line (DCO compliance)